package tcln.sors.service.core;

import java.util.List;

import org.dom4j.tree.AbstractEntity;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.InvalidValue;
import org.springframework.transaction.annotation.Transactional;

import tcln.commons.assertion.Assert;
import tcln.commons.collections.ArrayUtils;
import tcln.sors.model.core.criteria.SearchCriteria;
import tcln.sors.model.core.criteria.SortInfo;
import tcln.sors.model.core.dao.Dao;
import tcln.sors.model.core.dao.DaoFactory;
import tcln.sors.model.core.dao.DetachedCriteriaBuilder;
import tcln.sors.model.core.dom.Entity;
import tcln.sors.service.core.exception.HibernateEntityValidationException;
import tcln.sors.service.core.utils.BeanDuplicator;

/**
 * Default service implementation for an entity.
 * 
 * @param <E>
 *            Entity type.
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
@Transactional
public class DefaultEntityManagementServiceImpl<E extends Entity<Integer>> implements EntityManagementService<E> {

    /**
     * Current singleton DAO Factory.
     */
    protected DaoFactory daoFactory;

    /**
     * Current persistence entity class.
     */
    protected Class<E> persistenceClass;

    /**
     * A flag indicating if result entities will be detached from current persistence context before returning to caller
     * code.
     */
    protected boolean detachedOnReturning;

    /**
     * @param detachedOnReturning
     *            the new detachedOnReturning to set
     * @deprecated used only inside Spring configuration.
     */
    @Deprecated
    public void setDetachedOnReturning(boolean detachedOnReturning) {
        this.detachedOnReturning = detachedOnReturning;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void delete(E entity) {
        if (beforeDeleting(entity)) {
            Dao<E> entityDao = daoFactory.getDaoFor(persistenceClass);
            entityDao.delete(entity);
            afterDeleting(entity);
        }
    }

    /**
     * Called before given entity is really deleted.
     * 
     * @param entity
     * @return <code>true</code> to continue current action or <code>false</code> to cancel it.
     */
    protected boolean beforeDeleting(E entity) {
        return entity != null && entity.isPersisted();
    }

    /**
     * Called after given entity has been deleted from current database. <b>Remark</b>: This method is invoked only if
     * {@link #beforeDeleting(AbstractEntity)} returns <code>true</code> and there is DELETE action done.
     * 
     * @param deletedEntity
     *            Deleted entity. Notice that at the time this method is invoked, this entity is considered
     *            'non-existed'. This means, no operation leading to a lazy-loading or a saving action on this entity
     *            should be invoked.
     */
    protected void afterDeleting(E deletedEntity) {
        // nothing to do
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<E> search(SearchCriteria criteria, SortInfo sortInfo) {
        Assert.ARGUMENTS.notNull(criteria, "Search criteria is missing");
        Dao<E> entityDao = daoFactory.getDaoFor(persistenceClass);
        DetachedCriteria detachedCriteria = entityDao.createCriteria();
        DetachedCriteriaBuilder.create(detachedCriteria).addCriteria(criteria).addOrderCriteria(sortInfo);
        List<E> results = entityDao.findByCriteria(detachedCriteria);
        results = detachIfPossible(results);
        return afterListing(results);
    }

    /**
     * Detaches a given source entity from current persistence context.
     * 
     * @param source
     * @return Source object if {@link #detachedOnReturning} = <code>false</code> or the detached object otherwise.
     */
    protected E detachIfPossible(E source) {
        if (detachedOnReturning) {
            source = BeanDuplicator.createFor(persistenceClass, persistenceClass).duplicate(source);
        }
        return source;
    }

    /**
     * Detaches a given source entities from current persistence context.
     * 
     * @param sources
     * @return Source objects if {@link #detachedOnReturning} = <code>false</code> or the detached objects otherwise.
     */
    protected List<E> detachIfPossible(List<E> sources) {
        if (detachedOnReturning) {
            sources = BeanDuplicator.createFor(persistenceClass, persistenceClass).duplicateAll(sources);
        }
        return sources;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<E> listAll(SortInfo sortInfo) {
        Dao<E> entityDao = getDaoFactory().getDaoFor(persistenceClass);
        List<E> results = null;

        if (sortInfo == null) {
            results = entityDao.getAll();
        } else {
            DetachedCriteria criteria = entityDao.createCriteria();
            DetachedCriteriaBuilder.create(criteria).addOrderCriteria(sortInfo);
            results = entityDao.findByCriteria(criteria);
        }

        results = detachIfPossible(results);
        return afterListing(results);
    }

    /**
     * Called after given entities have been selected.
     * 
     * @param selectedEntities
     * @return Basically, this method returns the entities in parameter. However, it's not a restriction and new
     *         entities may be returned.
     */
    protected List<E> afterListing(List<E> selectedEntities) {
        return selectedEntities;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public E save(E entity) {
        if (beforeSaving(entity)) {
            E savedEntity = doSave(entity);
            savedEntity = detachIfPossible(savedEntity);
            return afterSaving(savedEntity);
        }
        return entity;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public E get(Integer objectId) {
        E entity = getDaoFactory().getDaoFor(persistenceClass).findByIdWeak(objectId);
        entity = detachIfPossible(entity);
        return entity;
    }

    /**
     * Actually performs 'save' action.
     * 
     * @param entity
     *            Unsaved entity.
     * @return Saved entity.
     */
    protected E doSave(E entity) {
        Dao<E> entityDao = daoFactory.getDaoFor(persistenceClass);
        return entityDao.saveOrUpdate(entity);
    }

    /**
     * Called before given entity is really saved.
     * 
     * @param entity
     * @return <code>true</code> to continue current action or <code>false</code> to cancel it.
     */
    protected boolean beforeSaving(E entity) {
        if (entity != null) {
            ClassValidator<E> validator = new ClassValidator<E>(persistenceClass);
            InvalidValue[] invalidValues = validator.getInvalidValues(entity);
            if (!ArrayUtils.isEmpty(invalidValues)) {
                throw new HibernateEntityValidationException(invalidValues,
                        "{0} invalid values were found. Details: {1}", invalidValues.length,
                        ArrayUtils.toString(invalidValues));
            }
            return true;
        }
        return false;
    }

    /**
     * Called after given entity has been saved to current database.
     * <p>
     * <b>Remark</b>: This method is invoked only if {@link #beforeSaving(AbstractEntity)} returns <code>true</code> and
     * there is SAVE action done.
     * 
     * @param savedEntity
     *            Saved entity.
     * @return Basically, this method returns the entity in parameter. However, it's not a restriction and a new entity
     *         may be returned.
     */
    protected E afterSaving(E savedEntity) {
        return savedEntity;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public void setPersistenceEntityClass(Class<?> entityClass) {
        this.persistenceClass = (Class<E>) entityClass;
    }

    /**
     * Returns current value of {@link #daoFactory}.
     * 
     * @return the {@link #daoFactory}.
     */
    public DaoFactory getDaoFactory() {
        return daoFactory;
    }

    /**
     * Sets new value for {@link #daoFactory}.
     * 
     * @param daoFactory
     *            the new daoFactory to set
     * @deprecated used by Spring container only.
     */
    @Deprecated
    public void setDaoFactory(DaoFactory daoFactory) {
        this.daoFactory = daoFactory;
    }

}
